# vBulletin Private Messaging component
# by Cee Pee

import forumapi
from deps.Form import Form

__all__ = [
	# Utilities
	"prepare_pm",
	
	# Extensible objects
	"PMFolder", "PrivateMessage",
]

## Utilities ##

def prepare_pm(session, pm=None, users=None, bcc=None, forward=False):
	"""Prepares to send a private message. To send a PM in reply to another
	message, use the `pm` parameter. Set `forward` to True to forward it
	instead of replying. `forward` without `pm` makes no sense.
	
	The `users` and `bcc` parameters can be used to specify the users to which
	the private message should be send to. `users` is required unless this is a
	reply to a PM (and not a forward).
	
	This returns a `PreparedPost` with the default title and message, including
	the quote if it's a reply to a PM, already set."""
	
	pm = PrivateMessage.obj(session, pm)
	
	if isinstance(users, list):
		users = [session.module.get_attr("User").obj(session, user) for user in users]
	elif users is not None:
		users = [session.module.get_attr("User").obj(session, users)]
	else:
		users = []
	
	identifier = None
	
	if pm:
		identifier = "pmid", pm.id
		
		if forward:
			if not users:
				raise(ValueError("Users to forward to required."))
		else:
			users[0:0] = [pm.source_user]
	elif not users:
		raise(ValueError("Users to send to required."))
	
	if isinstance(bcc, list):
		bcc = [session.module.get_attr("User").obj(session, user) for user in bcc]
	elif bcc is not None:
		bcc = [session.module.get_attr("User").obj(session, bcc)]
	else:
		bcc = []
	
	target_users = users + bcc
	
	form_url = "private.php?do=newpm{0}".format("&{0}={1}".format(*identifier) if identifier else "")
	
	form_page = session.get_page(form_url)
	
	soup = session.soup(form_page)
	
	if pm:
		session.retrieve_metadata(soup=soup, loc="postform", pm=pm)
	
	form_soup = soup.find("form", action=lambda url: "do=insertpm" in url)
	if not form_soup:
		raise(forumapi.PermissionError(session.get_error(soup) or "PM form not found."))
	
	post_data = Form(form_soup, session=session)
	
	max_users = int(form_soup.find("b").string)
	
	if len(target_users) > max_users:
		raise(forumapi.PermissionError("Not allowed to send PM to more than {0} users at a time.".format(max_users)))
	
	post_data["recipients"] = " ; ".join([str(user) for user in users])
	post_data["bccrecipients"] = " ; ".join([str(user) for user in bcc])
	
	post_info = session.module.get_attr("parse_quote")(post_data["message"])
	
	if pm and post_info:
		if not pm._source_user:
			pm._source_user = forumapi.User(session, name=post_info["username"])
		
		if not pm._content:
			pm._content = post_info["content"]
	
	return(session.module.get_attr("PreparedPost")(session, "sendpm", identifier, post_data, pm=pm, target_users=target_users, forward=forward))

## Hooks ##

def _retrieve_metadata_page(session=None, kw=None, **kwargs):
	if "pm" in kw:
		kw["pm"] = PrivateMessage.obj(session, kw["pm"])
		return(("private.php?do=showpm&pmid={0}".format(kw["pm"].id), "pm"))

def _retrieve_metadata(loc=None, kw=None, current=None, breadcrumb=None, **kwargs):
	if "pm" in kw:
		if loc == "pm":
			kw["pm"]._title = current
			if len(breadcrumb) > 0 and isinstance(breadcrumb[-1], PMFolder):
				kw["pm"]._folder = breadcrumb[-1]
		elif loc == "postform":
			if len(breadcrumb) > 1 and isinstance(breadcrumb[-2], PMFolder):
				kw["pm"]._folder = breadcrumb[-2]

def _get_breadcrumb(kw=None, breadcrumb=None, link_qs=None, name=None, **kwargs):
	if "folderid" in link_qs:
		return(("folderid", PMFolder))
	elif "pmid" in link_qs:
		if "pm" in kw:
			kw["pm"]._title = name
			breadcrumb.append(kw["pm"])
		else:
			return(("pmid", PrivateMessage))

def _prepared_post_submit(preparedpost=None, **kwargs):
	if preparedpost.type == "sendpm":
		# Grab from Sent Items folder
		folder = PMFolder(preparedpost.session, id=-1)
		
		folder.retrieve(pages=1)
		pm = folder.messages.first
		
		if pm.title != preparedpost.post_data["title"]:
			raise(forumapi.UnknownError("PM was not sent."))
		
		pm._content = preparedpost.post_data["message"]
		
		if "target_users" in preparedpost.kw:
			pm._target_users = preparedpost.kw["target_users"]
			
			if "pm" in preparedpost.kw and preparedpost.kw["pm"]:
				if "forward" in preparedpost.kw and preparedpost.kw["forward"]:
					preparedpost.kw["pm"]._forwarded = True
				else:
					preparedpost.kw["pm"]._replied = True
		
		return(pm)

forumapi.hooks.add("vb_retrieve_metadata_page", _retrieve_metadata_page)
forumapi.hooks.add("vb_retrieve_metadata", _retrieve_metadata)
forumapi.hooks.add("vb_get_breadcrumb", _get_breadcrumb)
forumapi.hooks.add("vb_prepared_post_submit", _prepared_post_submit)

## Classes ##

class PMFolder(forumapi.ExtensibleObject):
	"""A private messages folder. It can retrieve the PMs in it."""
	
	@classmethod
	def obj(cls, session, *a, **kw):
		if isinstance(a[0], cls):
			return(a[0])
		elif isinstance(a[0], int):
			return(cls(session, *a[1:], id=a[0], **kw))
		elif isinstance(a[0], str):
			return(cls(session, *a[1:], name=a[0], **kw))
		else:
			return(None)
	
	def __init__(self, session, id=None, name=None, messages=None):
		if not(id is not None or name):
			raise(ValueError("ID or name required."))
		
		self.session = session
		
		self._id = id
		self._name = name
		
		if isinstance(messages, list):
			self._messages = [PrivateMessage.obj(session, pm) for pm in messages]
		else:
			self._messages = messages
		
		self._find_instances()
	
	def _merge(self, other):
		if self._id is not None:
			other._id = self._id
		if self._name:
			other._name = self._name
		
		self._merge_obj(other, "_messages")
	
	@classmethod
	def get_folders(cls, session):
		if hasattr(session, "pm_folders"):
			return(session.pm_folders)
		else:
			default = PMFolder(session, id=0)
			default.retrieve()
			
			return(session.pm_folders)
	
	@property
	def data(self):
		return({
			"id": self._id,
			"name": self._name,
			"messages": forumapi._flatten_data(self._messages),
		})
	
	@property
	def id(self):
		if self._id is not None:
			return(self._id)
		else:
			self.__class__.get_folders(self.session)
			return(self._id)
	
	@property
	def name(self):
		if self._name:
			return(self._name)
		else:
			self.retrieve()
			return(self._name)
	
	@property
	def messages(self):
		if self._messages is not None:
			return(self._messages)
		else:
			self.retrieve()
			return(self._messages)
	
	def retrieve(self, pm=None, pages=None):
		"""Obtains the private messages and other metadata from the folder.
		
		By default only retrieves the first page of the folder; more pages can
		be retrieved using the `pages` parameter or a `pm` parameter
		representing a `PrivateMessage` that is to be found in the folder. See
		`Session.get_pages()` for more information."""
		
		if pages is None:
			if pm:
				pages = "all"
			else:
				pages = 1
		
		response = self.session.get_page("private.php?folderid={0}".format(self.id))
		
		page_soup = self.session.soup(response)
		
		form_soup = page_soup.find("form", {"action": lambda url: "private.php" in url})
		if not form_soup:
			raise(forumapi.PermissionError(self.session.get_error(page_soup) or "PM Folder does not exist."))
		
		self._name = form_soup.find(id="nav_pmfolders.listinfo").contents[0]
		
		if not hasattr(self.session, "pm_folders"):
			self.session.pm_folders = []
			
			pmjump = form_soup.find("select", {"name": "folderid"})
			for folder_option in pmjump.findAll("option"):
				self.session.pm_folders.append(PMFolder(self.session, id=int(folder_option["value"]), name=str(folder_option.string)))
		
		if self._messages is None:
			self._messages = self.session.get_pages(self.get_list, response=response, pages=pages, pm=pm)
		else:
			self._messages.get_pages(response=response, pages=pages, pm=pm)
	
	def get_list(self, result, page, page_soup, first_page=False, pm=None, **kw):
		"""Given a `BeautifulSoup` of a page, adds `PrivateMessage` objects for
		the messages on the page of the folder."""
		
		if first_page and pm and pm in result.items:
			return(False)
		
		form_soup = page_soup.find("form", action=lambda url: url and "do=managepm" in url)
		
		if not form_soup:
			return(False)
		
		continue_pages = True
		
		table_soup = form_soup.find("table", "tborder")
		for row in table_soup.findAll("tr"):
			cells = row.findAll("td")
			if len(cells) == 4:
				# It's actually a PM
				icon_url = cells[0].find("img")["src"]
				
				if "_new" in icon_url:
					read = False
				else:
					read = True
				
				if "_replied" in icon_url:
					replied = True
				else:
					replied = False
				
				if "_forwarded" in icon_url:
					forwarded = True
				else:
					forwarded = False
				
				link = cells[2].find("a")
				
				id = int(forumapi.qs_dict_from_url(link["href"])["pmid"][0])
				if link.string:
					title = str(link.string)
				else:
					title = str(link.strong.string)
				
				users = []
				date = None
				time = None
				for span in cells[2].findAll("span"):
					contents = str(span.string)
					attrs = dict(span.attrs)
					if "onclick" in attrs:
						user_id = int(self.session.onclick_pattern.match( attrs["onclick"]).group(1))
						user_name = contents
						
						users.append(forumapi.User(self.session, id=user_id, name=user_name))
					elif "class" in attrs and attrs["class"] == "smallfont":
						date = contents
					elif "class" in attrs and attrs["class"] == "time":
						time = contents
				
				if date and time:
					datetime = self.session.get_datetime([date, time])
				else:
					datetime = None
				
				if self.id == -1:
					source_user = self.session.user
					target_users = users
				else:
					source_user = users[0]
					# There might be more than just me
					target_users = None
				
				message = PrivateMessage(self.session, id=id, folder=self, source_user=source_user, target_users=target_users, title=title, time=datetime, read=read, replied=replied, forwarded=forwarded, form=form_soup)
				
				if pm == message:
					continue_pages = False
				
				result.data[page].append(message)
		
		return(continue_pages)
	
	def __eq__(self, other):
		if isinstance(other, PMFolder):
			if self._id is not None and other._id is not None:
				return(self.id == other.id)
			else:
				return(self.name == other.name)
		elif isinstance(other, int):
			return(self.id == other)
		elif isinstance(other, str):
			return(self.name == other)
		else:
			return(False)
	
	def __int__(self):
		return(self.id)
	
	def __str__(self):
		return(self.name)
	
	def __repr__(self):
		return("<PMFolder {self.id} \"{self.name}\">".format(self=self))
		
class PrivateMessage(forumapi.ExtensibleObject):
	"""A private message. It has a folder and sending and receiving users
	associated with it. It will be able to send a reply on it or forward it."""
	
	@classmethod
	def obj(cls, session, *a, **kw):
		if isinstance(a[0], cls):
			return(a[0])
		elif isinstance(a[0], int):
			return(cls(session, *a[1:], id=a[0], **kw))
		elif isinstance(a[0], str):
			return(cls(session, *a[1:], name=a[0], **kw))
		else:
			return(None)
	
	def __init__(self, session, id=None, folder=None, source_user=None, target_users=None, title=None, content=None, time=None, read=None, replied=None, forwarded=None, form=None):
		if not(id or (folder and title)):
			raise(ValueError("ID or folder and title required."))
		
		self.session = session
		
		self._id = id
		
		self._folder = PMFolder.obj(session, folder)
		
		self._source_user = self.module.get_attr("User").obj(session, source_user)
		
		if isinstance(target_users, list):
			self._target_users = [self.module.get_attr("User").obj(session, user) for user in target_users]
		else:
			user = self.module.get_attr("User").obj(session, target_users)
			if user is not None:
				self._target_users = [user]
			else:
				self._target_users = None
		
		self._title = title
		self._content = content
		
		self._time = time
		
		self._read = read
		self._replied = replied
		self._forwarded = forwarded
		
		self._form = None
		self._folder_form = form
		
		self._find_instances()
	
	def _merge(self, other):
		if self._id:
			other._id = self._id
		
		if self._folder:
			other._folder = self._folder
		
		if self._source_user:
			other._source_user = self._source_user
		if self._target_users is not None:
			other._target_users = self._target_users
		
		if self._title:
			other._title = self._title
		
		self._merge_obj(other, "_content")
		
		if self._time:
			other._time = self._time
		
		if self._read is not None:
			other._read = self._read
		if self._replied is not None:
			other._replied = self._replied
		if self._forwarded is not None:
			other._forwarded = self._forwarded
		
		if self._form:
			other._form = self._form
		if self._folder_form:
			other._folder_form = self._folder_form
	
	@property
	def data(self):
		return({
			"id": self._id,
			"folder": int(self.folder) if self._folder else None,
			"source_user": int(self.source_user) if self._source_user else None,
			"target_users": [int(user) for user in self.target_users] if self._target_users is not None else None,
			"title": self._title,
			"content": forumapi._flatten_data(self._content),
			"time": self._time,
			"read": self._read,
			"replied": self._replied,
			"forwarded": self._forwarded,
		})
	
	@property
	def id(self):
		if self._id:
			return(self._id)
		else:
			self.folder.retrieve(pm=self)
			return(self._id)
	
	@property
	def folder(self):
		if self._folder:
			return(self._folder)
		else:
			self.retrieve()
			return(self._folder)
	
	@property
	def source_user(self):
		if self._source_user is not None:
			return(self._source_user)
		else:
			self.retrieve()
			return(self._source_user)
	
	@property
	def target_users(self):
		if self._target_users is not None:
			return(self._target_users)
		else:
			self.retrieve()
			return(self._target_users)
	
	@property
	def title(self):
		if self._title:
			return(self._title)
		else:
			self.retrieve()
			return(self._title)
	
	@property
	def content(self):
		if self._content:
			return(self._content)
		else:
			self.session.prepare_pm(pm=self)
			return(self._content)
	
	@property
	def time(self):
		if self._time:
			return(self._time)
		else:
			self.retrieve()
			return(self._time)
	
	@property
	def read(self):
		if self._read is not None:
			return(self._read)
		elif self._folder:
			# Use folder so as to not mark it read
			# It's no use if we don't have ._folder, though
			self.folder.retrieve(pm=self)
			return(self._read)
		else:
			self.retrieve()
			return(self._read)
	
	@read.setter
	def read(self, value):
		self.mark_read(read=value)
	
	@property
	def replied(self):
		if self._replied is not None:
			return(self._replied)
		else:
			self.folder.retrieve(pm=self)
			return(self._replied)
	
	@property
	def forwarded(self):
		if self._forwarded is not None:
			return(self._forwarded)
		else:
			self.folder.retrieve(pm=self)
			return(self._forwarded)
	
	@property
	def form(self):
		if self._form:
			return(self._form)
		else:
			self.retrieve()
			return(self._form)
	
	@property
	def folder_form(self):
		if self._folder_form:
			return(self._folder_form)
		else:
			self.folder.retrieve(pm=self)
			return(self._folder_form)
	
	def do_form(self, what):
		"""Submits the PM folder form with an action taken on the message,
		depending on the `what` parameter."""
		
		form = Form(self.folder_form, session=self.session)
		form.get("pm[{0}]".format(self.id)).set()
		form["dowhat"] = what
		
		form.submit()
	
	def mark_read(self, read=True):
		"""Marks the message as read, or as unread, depending on the `read`
		parameter."""
		
		if self._read != read:
			if read:
				self.do_form("read")
				self._read = True
			else:
				self.do_form("unread")
				self._read = False
	
	def delete(self):
		"""Deletes the message."""
		
		if self._form:
			self.form.get("deletepm").set()
			self.form.submit()
		else:
			self.do_form("delete")
		
		if self._folder and self._folder._messages is not None and self in self._folder._messages:
			self._folder._messages.remove(self)
	
	def reply(self, message, title=None, users=None, bcc=None, forward=False):
		"""Given a message, sends a reply to the private message.
		
		See `prepare_pm` and `forumapi.PreparedPost` for more details."""
		
		pm = prepare_pm(self.session, pm=self, users=users, bcc=None, forward=forward)
		
		if title:
			pm.title = title
		
		if message:
			pm.message += message
		
		return(pm.submit())
	
	def retrieve(self):
		"""Obtains information from the private message page."""
		
		page = self.session.get_page("private.php?do=showpm&pmid={0}".format(self.id))
		
		page_soup = self.session.soup(page)
		
		form = page_soup.find("form", action=lambda url: url and "do=managepm" in url)
		
		if not form:
			raise(forumapi.PermissionError(self.session.get_error(soup) or "PM does not exist."))
		
		self.session.retrieve_metadata(soup=page_soup, loc="pm", pm=self)
		
		table = form.find("table", "tborder")
		recipients = table.find("td", "alt1")
		
		if recipients:
			self._target_users = []
			for span in recipients.findAll("span"):
				attrs = dict(span.attrs)
				if "onclick" in attrs:
					user_id = int(self.session.onclick_pattern.match( attrs["onclick"]).group(1))
					user_name = str(span.string)
					
					self._target_users.append(forumapi.User(self.session, id=user_id, name=user_name))
		else:
			self._target_users = [self.session.user]
		
		post = page_soup.find(id="post")
		
		post_data = self.module.get_attr("parse_postbit")(self.session, post)
		
		self._source_user = post_data["user"]
		self._title = post_data["title"]
		self._content = post_data["content"]
		self._time = post_data["time"]
		self._read = post_data["read"]
		
		self._form = Form(form, session=self.session)
	
	def __eq__(self, other):
		if isinstance(other, PrivateMessage):
			if not(self._id and other._id) and self._folder and other._folder:
				return(self.folder == other.folder and self.title == other.title)
			else:
				return(self.id == other.id)
		elif isinstance(other, int):
			return(self.id == other)
		elif isinstance(other, str):
			return(self.title == other)
		else:
			return(False)
	
	def __str__(self):
		return(self.title)
	
	def __int__(self):
		return(self.id)
	
	def __repr__(self):
		return("<PrivateMessage {self.id} \"{self.title}\"{0}{1} in \"{self.folder.name}\">".format(" by {self._source_user}".format(self=self) if self._source_user else "", " to {0}".format(", ".join([str(user) for user in self._target_users])) if self._target_users else "", self=self))
